﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using SS.Common.Logging;

namespace SS.Common.Utilities
{
    [Serializable]
    public class ValidationException : Exception
    {
        public string[] Errors { get; set; }

        public ValidationException(string message, params string[] errors)
            :base(message)
        {
            this.Errors = errors;
        }

        protected ValidationException(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            this.Errors = (string[])info.GetValue("Errors", typeof(string[]));
        }

        /// <summary>
        /// The get object data.
        /// </summary>
        /// <param name="info">
        /// The info.
        /// </param>
        /// <param name="context">
        /// The context.
        /// </param>
        public override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            base.GetObjectData(info, context);
            info.AddValue("Errors", this.Errors, typeof(string[]));
        }
    }

    /// <summary>
    /// XML helper class for serialize/deserialize
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class XMLHelper<T> where T:class
    {
        private static readonly ILogger Logger = LoggerManager.GetLogger(typeof(XMLHelper<T>).Name);

        /// <summary>
        /// Convert to string in xml format
        /// </summary>
        /// <param name="obj">Object to convert</param>
        /// <returns></returns>
        public static string ToString(T obj)
        {
            var xmlSrlz = new XmlSerializer(typeof(T));
            var sw = new StringWriter();
            xmlSrlz.Serialize(sw, obj);
            return sw.ToString();            
        }

        /// <summary>
        /// Create XML file from an object
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public static bool ToFile(T obj, string filePath)
        {
            try
            {
                SaveToFile(obj, filePath);
                return true;
            }
            catch (Exception e)
            {
                Logger.Error(string.Format("Error occurs when EFP save obj to file [{0}]", filePath), e);
                return false;
            }
        }

        public static void SaveToFile(T obj, string filePath)
        {
            var xmlSrlz = new XmlSerializer(typeof(T));
            using (var fs = new FileStream(filePath, FileMode.Create))
            {
                xmlSrlz.Serialize(fs, obj);
                fs.Flush();
                fs.Close();
            }
        }
        /// <summary>
        /// Load object from xml format string
        /// </summary>
        /// <param name="xmlString">xml string</param>
        /// <returns></returns>
        public static T Load(string xmlString)
        {
            var xmlSrlz = new XmlSerializer(typeof(T));
            var sw = new StringReader(xmlString);
            return (T)xmlSrlz.Deserialize(sw);
        }

        /// <summary>
        /// Load object from file
        /// </summary>
        /// <param name="filename">file path</param>
        /// <returns></returns>
        public static T LoadFromFile(string filename)
        {
            var fs = new FileStream(filename, FileMode.Open, FileAccess.Read);
            return LoadFromStream(fs);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="path"></param>
        /// <param name="md5String"></param>
        /// <returns></returns>
        public static T LoadFromFile(string path, out string md5String)
        {
            var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            return LoadFromStream(fileStream, out md5String);
        }

        /// <summary>
        /// Validate xml file and deserialize to object
        /// </summary>
        /// <param name="xmlPath"></param>
        /// <param name="xsdPaths"></param>
        /// <returns></returns>
        public static T LoadFromFile(string xmlPath, params string[] xsdPaths)
        {
            string md5String;
            return LoadFromFile(xmlPath, out md5String, xsdPaths);
        }

        public static T LoadFromFile(string xmlPath, out string md5String, params string[] xsdPaths)
        {
            var errs = ValidateXml(xmlPath, xsdPaths);
            if (errs != null && errs.Count > 0)
            {
                throw new ValidationException(string.Format("{0} is invalid", xmlPath), errs.ToArray());
            }

            return LoadFromFile(xmlPath, out md5String);
        }
        /// <summary>
        /// Load object from a stream
        /// </summary>
        /// <returns></returns>
        public static T LoadFromStream(Stream str)
        {
            using (str)
            {
                var s = new XmlSerializer(typeof(T));
                var xmlTextReader = new XmlTextReader(str);
                return (T)s.Deserialize(xmlTextReader);
            }
        }

        public static T LoadFromStream(Stream str, out string md5String)
        {
            using (str)
            {
                var s = new XmlSerializer(typeof(T));
                var xmlTextReader = new XmlTextReader(str);
                var instance = (T)s.Deserialize(xmlTextReader);
                using (var md5 = new MD5CryptoServiceProvider())
                {
                    str.Seek(0, SeekOrigin.Begin);
                    md5String = BitConverter.ToString(md5.ComputeHash(str));
                }
                return instance;
            }
        }

        /// <summary>
        /// Validate xml file by many separated schema files
        /// </summary>
        /// <param name="xmlPath">The xml file path</param>
        /// <param name="xsdPaths">Xsd paths</param>
        /// <exception cref="IOException">Throw IOEception when xml file  does not exists</exception>
        /// <returns></returns>
        /// <remarks>The validation process will ignore the missing schema</remarks>
        private static IList<string> ValidateXml(string xmlPath, params string[] xsdPaths)
        {
            if (xsdPaths == null)
            {
                Logger.Warn("Cannot validate xml because it does't have xsd");
                return null;
            }

            // Check schema files exist
            var xsdSet = new XmlSchemaSet();
            foreach (var xsdPath in xsdPaths)
            {
                if (File.Exists(xsdPath))
                {
                    xsdSet.Add(null, xsdPath);
                }
                else
                {
                    var err = string.Format("Error occurs while validating [{0}]: " +
                                            "Schema [{1}] does not exist", xmlPath, xsdPath);
                    Logger.Error(err);
                    throw new Exception(err);
                }
            }

            var xmlSetting = new XmlReaderSettings()
            {
                ValidationType = ValidationType.Schema,
                IgnoreComments = true,
                Schemas = xsdSet
            };

            var xmlErrors = new List<string>();
            ValidationEventHandler handler = (sender, e) =>
            {
                if (e.Severity == XmlSeverityType.Error)
                {
                    xmlErrors.Add(e.Message);
                    Logger.Error(e.Message, e.Exception);
                }
            };
            xmlSetting.ValidationEventHandler += handler;
            
            using (var validateReader = XmlReader.Create(xmlPath, xmlSetting))
            {
                while (validateReader.Read())
                {
                    
                }
                validateReader.Close();
            }

            Logger.Debug("XMLHelper.ValidateXml() - Leave");

            xmlSetting.ValidationEventHandler -= handler;

            return xmlErrors;
        }
    }
}
